/*
 * 2007/05
 * 2007/10
 * 2011/11/17 - 13:47
 */
package org.nettyflux.example.pong;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.AffineTransformOp;
import java.awt.geom.AffineTransform;
import java.io.IOException;

/**
 * @author Victor Williams Stafusa da Silva
 */
public class Ball implements ScreenObject {

    public static final int RADIUS = 7;
    private static final double ANGLE_135_DEGREES = (3.0 * Math.PI / 4.0);
    private static final double ANGLE_67_5_DEGREES = (3.0 * Math.PI / 8.0);

    private static final BufferedImage SPRITE1;
    private static final BufferedImage SPRITE2;

    static {
        try {
            SPRITE1 = MaskedImage.load("ball1.bmp", 0xffffff);
            SPRITE2 = MaskedImage.load("ball2.bmp", 0xffffff);
        } catch (IOException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private final RenderScreen screen;
    private final BufferedImage sprite;
    private double x;
    private double y;

    private final double initialX;
    private double vx;
    private double vy;
    private double speed;
    private double angularPhase;
    private double angularSpeed;
    private int boundsCount;

    public Ball(int x, int y, RenderScreen screen, int ballNumber) {
        this.x = x;
        this.y = y;
        this.screen = screen;
        this.initialX = x;
        this.speed = 18;
        this.sprite = (ballNumber == 1) ? SPRITE1 : SPRITE2;
        vx = (Math.sqrt(2) * speed) / 2;
        if (ballNumber == 2) vx *= -1;
        vy = (Math.sqrt(2) * speed) / 2;
        angularSpeed = (Math.random() - 0.5) / 3;
    }

    @Override
    public void move(double deltaT) {

        double minX = RADIUS;
        double maxX = screen.getRenderWidth() - RADIUS;
        double minY = RADIUS;
        double maxY = screen.getRenderHeight() - RADIUS;

        boolean reboundX = false, reboundY = false;
        Paddle reboundPaddle = null;
        double subDeltaT = 0;
        angularPhase += angularSpeed;
        angularPhase %= (Math.PI * 2);

        while (deltaT != 0.0) {
            double projectedX = x + vx * deltaT;
            double projectedY = y + vy * deltaT;

            reboundX = false; reboundY = false; reboundPaddle = null;

            // Test right wall collision.
            if (projectedX > maxX) {
                restart();
                screen.getRightPaddle().decreaseLifes();
                break;
            }

            // Test left wall collision.
            if (projectedX < minX) {
                restart();
                screen.getLeftPaddle().decreaseLifes();
                break;
            }

            // Test left and right wall collision.
            //if (!isInRange(minX, projectedX, maxX)) {
            //    subDeltaT = (clamp(minX, projectedX, maxX) - x) / vx;
            //    reboundX = true;
            //}

            // Test top and bottom wall collision.
            if (!isInRange(minY, projectedY, maxY)) {
                subDeltaT = (clamp(minY, projectedY, maxY) - y) / vy;
                reboundY = true;
            }

            for (Paddle p : screen.getPaddles()) {
                double minPaddleX = p.getX1() - RADIUS;
                double maxPaddleX = p.getX2() + RADIUS;
                double minPaddleY = p.getY1() - RADIUS;
                double maxPaddleY = p.getY2() + RADIUS;
                boolean xCollides = isInRange(minPaddleX, projectedX, maxPaddleX);
                boolean yCollides = isInRange(minPaddleY, projectedY, maxPaddleY);
                boolean leftCollides = isInRange(x, minPaddleX, projectedX) && yCollides;
                boolean rightCollides = isInRange(projectedX, maxPaddleX, x) && yCollides;
                boolean topCollides = isInRange(y, minPaddleY, projectedY) && xCollides;
                boolean bottomCollides = isInRange(projectedY, maxPaddleY, y) && xCollides;

                // Test left and right paddle border collision.
                if (leftCollides || rightCollides) {
                    subDeltaT = (clamp(minPaddleX, projectedX, maxPaddleX) - x) / vx;
                    reboundX = true;
                }

                // Test top and bottom paddle border collision.
                if (topCollides || bottomCollides) {
                    subDeltaT = (clamp(minPaddleY, projectedY, maxPaddleY) - y) / vy;
                    reboundY = true;
                }

                if (leftCollides || rightCollides) {
                    reboundPaddle = p;
                }
                /*if ((leftCollides && !p.isLeftPaddle()) || (rightCollides && p.isLeftPaddle())) {
                    reboundPaddle = p;
                }*/
            }

            // No collision. Move freely.
            if (!reboundX && !reboundY && reboundPaddle == null) {
                subDeltaT = deltaT;
            }

            // The remaining time after the collision.
            deltaT -= subDeltaT;

            // Move this ball.
            if (reboundPaddle == null) {
                y += vy * subDeltaT;
                x += vx * subDeltaT;
                if (reboundX) vx = -vx;
                if (reboundY) vy = -vy;
            } else {
                rebound(reboundPaddle);
            }
        }
    }

    private static boolean isInRange(double min, double mid, double max) {
        return min <= mid && mid <= max;
    }

    private static double clamp(double min, double mid, double max) {
        return min > mid ? min : max < mid ? max : mid;
    }

    public void rebound(Paddle rebounded) {
        boundsCount++;
        if (boundsCount % 10 == 0) speed *= 1.1;
        int sgn = (vx > 0 ? -1 : vx < 0 ? 1 : 0); // Current speed signal * -1.
        double rnd = (Math.random() * ANGLE_135_DEGREES) - ANGLE_67_5_DEGREES; // -67.5 degrees to +67.5 degrees.
        vx = speed * sgn * Math.cos(rnd);
        vy = speed * Math.sin(rnd);
        angularSpeed = (Math.random() - 0.5) / 3;
        rebounded.increasePoints();
    }

    public void restart() {
        x = initialX;
        speed = 18;
        boundsCount = 0;
    }

    public double getSpeed() {
        return speed;
    }

    public double getAngularPhase() {
        return angularPhase;
    }

    private static final RenderingHints HINTS = new RenderingHints(null);

    static {
        HINTS.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        HINTS.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        HINTS.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        HINTS.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    }

    @Override
    public void draw(Graphics2D g) {
        // drawOval sums a pixel to width and height, fillOval doesn't.
        /*g.setColor(Color.GREEN);
        g.fillOval(round(x) - RAIO, round(y) - RADIUS, 2 * RADIUS + 1, 2 * RADIUS + 1);
        g.setColor(Color.BLUE);
        g.drawOval(round(x) - RAIO, round(y) - RADIUS, 2 * RADIUS, 2 * RADIUS);*/

        AffineTransform at = AffineTransform.getRotateInstance(angularPhase, RADIUS, RADIUS);
        AffineTransformOp ato = new AffineTransformOp(at, HINTS);
        g.drawImage(sprite, ato, (int) Math.round(x) - RADIUS, (int) Math.round(y) - RADIUS);
    }
}